package imse.team1.inventory.ws;

import imse.team1.errs.InternalErrorElement;
import imse.team1.inventory.UpdateOrderRespElement;
import imse.team1.inventory.dal.InventoryQueryService;
import imse.team1.orders.data.*;

import java.util.*;

/**
 * @author Eugen, 6/19/12 1:35 PM
 */
public class InventoryServiceSkeleton implements InventoryServiceSkeletonInterface {
    /* This field allows us to store the unusued warehouses for a given order. They are stored also
    * in increasing distance from client location. This cache is reused when adding items to an already
    * made order that couldn't be found in the warehouses that are being already used for that order*/
    private static Map<String, List<String>> unusedWarehouseCache = new HashMap<String, List<String>>(); // not synchronizing the access to it in code cause it will not be tested in a concurrent setting

    private InventoryQueryService queryService = new InventoryQueryService();

    public Order bookOrder(ClientID clientID, Location clientLocation, OrderItem[] orderItems, Location[] warehouseLocations) throws UnsuccessfulOrderStatusMessage, InternalFaultMessage {
        List<PackageUnit> packageUnits = new ArrayList<PackageUnit>();
        List<OrderItem> remainingOrderItems = new ArrayList<OrderItem>();
        List<OrderItem> missingItems = new ArrayList<OrderItem>();
        Order order = null;
        int unusuedWarehouseIndex = -1;
        for (OrderItem item : orderItems) {
            remainingOrderItems.add(item);
        }
        try {
            for (Location warehouseLocation : warehouseLocations) {
                ++unusuedWarehouseIndex;
                missingItems = queryService.bookItems(remainingOrderItems, warehouseLocation.getCity());
                if (missingItems == null) {
                    PackageUnit lastPackageUnit = new PackageUnit();
                    lastPackageUnit.setWarehouse(warehouseLocation);
                    lastPackageUnit.setOrderItem(remainingOrderItems.toArray(new OrderItem[0]));
                    // create order
                    order = new Order();
                    order.setId(clientID.getClientID() + "-" + System.currentTimeMillis() + "");
                    order.setClientid(clientID);
                    order.setDestination(clientLocation);
                    packageUnits.add(lastPackageUnit);
                    order.setPackageUnit(packageUnits.toArray(new PackageUnit[0]));
                    order.setTotalCosts(0);
                    order.setTransportationCosts(0);
                    order.setState(OrderStatusEnum.BOOKED);
                    remainingOrderItems.clear();
                    break;
                } else {
                    PackageUnit packageUnit = new PackageUnit();
                    packageUnit.setWarehouse(warehouseLocation);
                    List<OrderItem> packagedOrderItems = new ArrayList<OrderItem>();
                    OrderItem[] itemArray = remainingOrderItems.toArray(new OrderItem[0]);
                    // iterate over a copy of the array if removing the items from the list
                    // while iterating over it to avoid ConcurrentModificationException
                    boolean toDelete[] = new boolean[remainingOrderItems.size()];
                    for (int i = 0; i < toDelete.length; i++)
                        toDelete[i] = false;
                    for (int i = 0; i < itemArray.length; i++) {
                        OrderItem remainingItem = itemArray[i];
                        boolean foundInMissing = false;
                        for (OrderItem missingItem : missingItems) {
                            if (missingItem.getName().equals(remainingItem.getName())) {
                                foundInMissing = true;
                                if (remainingItem.getQty() > missingItem.getQty()) {
                                    // some quantity for this wine was booked in the current inventory but not all quantity
                                    OrderItem bookedItem = new OrderItem();
                                    bookedItem.setName(remainingItem.getName());
                                    bookedItem.setQty(remainingItem.getQty() - missingItem.getQty());
                                    packagedOrderItems.add(bookedItem);
                                }
                                remainingItem.setQty(missingItem.getQty());
                                break;
                            }
                        }
                        if (foundInMissing == false) {
                            OrderItem bookedItem = new OrderItem();
                            bookedItem.setName(remainingItem.getName());
                            bookedItem.setQty(remainingItem.getQty());
                            packagedOrderItems.add(bookedItem);
                            toDelete[i] = true;
                        }
                    }
                    remainingOrderItems.clear();
                    for (int i = 0; i < itemArray.length; i++) {
                        if (toDelete[i] == false) {
                            remainingOrderItems.add(itemArray[i]);
                        }
                    }
                    if (packagedOrderItems.size() > 0) {
                        packageUnit.setOrderItem(packagedOrderItems.toArray(new OrderItem[0]));
                        packageUnits.add(packageUnit);
                    }
                }
            }
        } catch (Exception ex) {
            undoChanges(packageUnits);
            throw new InternalFaultMessage("An error occured while booking order in warehouses ! Try again later.");
        }
        if (remainingOrderItems.size() > 0) {
            // the order could not be fullfilled: rollback all bookings and return an Unsuccessful status
            undoChanges(packageUnits);
            UnsuccessfulOrderStatusMessage missingItemsException = new UnsuccessfulOrderStatusMessage();
            OrderStatusE unsuccessfulOrderStatus = new OrderStatusE();
            OrderStatus uStatus = new OrderStatus();
            uStatus.setStatus(StatusEnum.UNSUCCESSFUL);
            OrderStatusChoice_type0 payload = new OrderStatusChoice_type0();
            payload.setMissingItem(remainingOrderItems.toArray(new OrderItem[0]));
            uStatus.setOrderStatusChoice_type0(payload);
            unsuccessfulOrderStatus.setOrderStatus(uStatus);
            missingItemsException.setFaultMessage(unsuccessfulOrderStatus);
            throw missingItemsException;
        }
        // populate the cache of unused warehouses
        List<String> unusedWarehouses = new ArrayList<String>();
        for (Location loc : warehouseLocations) {
            boolean foundWarehouse = false;
            for (PackageUnit pu : order.getPackageUnit()) {
                if (pu.getWarehouse().getCity().equals(loc.getCity())) {
                    foundWarehouse = true;
                    break;
                }
            }
            if (foundWarehouse == false){
                unusedWarehouses.add(loc.getCity().intern());
            }
        }
        if (unusedWarehouses.size() > 0) {
            unusedWarehouseCache.put(order.getId(), unusedWarehouses);
        }
        return order;
    }

    public String cancelOrder(Order order) {
        for (PackageUnit pu : order.getPackageUnit()) {
            String warehouseId = pu.getWarehouse().getCity();
            try {
                queryService.removeBookedItems(Arrays.asList(pu.getOrderItem()), warehouseId);
            } catch (Exception e) { // removal can't have business errors
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }
        unusedWarehouseCache.remove(order.getId());
        return "OK";
    }

    public UpdateOrderRespElement updateOrder(Order order, UpdateType updateType, OrderItemList itemList) throws InternalFaultMessage {
        UpdateStatusResponse status = null;
        OrderItem[] items = itemList.getItem();
        if (updateType.equals(UpdateType.ADD)) {
            try {
                status = addItems(order, items);
            } catch (Exception ex) {
                InternalFaultMessage businessEx = new InternalFaultMessage();
                InternalErrorElement message = new InternalErrorElement();
                message.setReason("Error encountered while trying to add items. Update failed.");
                businessEx.setFaultMessage(message);
                throw businessEx;
            }
        } else if (updateType.equals(UpdateType.REMOVE)) {
            try {
                status = removeItems(order, items);
            } catch (Exception ex) {
                InternalFaultMessage businessEx = new InternalFaultMessage();
                InternalErrorElement message = new InternalErrorElement();
                message.setReason("Error encountered while trying to remove items. Update failed.");
                businessEx.setFaultMessage(message);
                throw businessEx;
            }
        }
        UpdateOrderRespElement response = new UpdateOrderRespElement();
        response.setUpdateStatusResponse(status);
        response.setUpdatedOrder(order);
        return response;
    }

    private UpdateStatusResponse removeItems(Order order, OrderItem[] itemsToRemove) throws Exception {
        // we assume the client won't remove all itemsToRemove since there is a simple cancel operation for that
        UpdateStatusResponse status = new UpdateStatusResponse();
        Map<Location, List<OrderItem>> warehousesForItem = new HashMap<Location, List<OrderItem>>();
        // check first if itemsToRemove to be removed where present in the order
        for (OrderItem itemToRemove : itemsToRemove) {
            boolean itemFound = false;
            int quantityToRemove = itemToRemove.getQty();
            itemSearch:
            for (PackageUnit pu : order.getPackageUnit()) {
                for (OrderItem orderedItem : pu.getOrderItem()) {
                    if (itemToRemove.getName().equals(orderedItem.getName())) {
                        itemFound = true;
                        int q = Math.min(quantityToRemove, orderedItem.getQty());// if to remove is less than total available in order
                        List<OrderItem> itemsForWarehouse = warehousesForItem.get(pu.getWarehouse());
                        if (itemsForWarehouse == null) {
                            itemsForWarehouse = new ArrayList<OrderItem>();
                            warehousesForItem.put(pu.getWarehouse(), itemsForWarehouse);
                        }
                        OrderItem item = new OrderItem();
                        item.setName(itemToRemove.getName());
                        item.setQty(q);
                        itemsForWarehouse.add(item);
                        quantityToRemove -= q;
                        if (quantityToRemove == 0)
                            break itemSearch;
                        else {
                            break;
                        }
                    }
                }
            }
            if (!itemFound) {
                UpdateIssues missingItems = status.getIssues();
                if (missingItems == null)// first time
                {
                    missingItems = new UpdateIssues();
                    status.setIssues(missingItems);
                }
                missingItems.addIssue("Item: '" + itemToRemove.getName() + "' was missing from previously made order");
                break;
            } else {
                if (quantityToRemove > 0) {
                    // client requested to remove more than was in order
                    UpdateIssues badItems = status.getIssues();
                    if (badItems == null) {
                        badItems = new UpdateIssues();
                        status.setIssues(badItems);
                        badItems.addIssue("More quantity to remove for Item: '" + itemToRemove.getName() + "' was requested than originally made !");
                        break;
                    }
                }
            }
        }
        if (status.getIssues() != null) {
            status.setStatus(UpdateStatus.CANCELLED);
            return status;
        } else {// all itemsToRemove are valid
            for (Location warehouse : warehousesForItem.keySet()) {
                queryService.removeBookedItems(warehousesForItem.get(warehouse), warehouse.getCity());
            }
            // no exceptions -> return OK status
            status.setStatus(UpdateStatus.OK);
            updateWarehousesInOrderAfterRemoval(order, warehousesForItem);
            return status;
        }
    }

    private void updateWarehousesInOrderAfterRemoval(Order order, Map<Location, List<OrderItem>> warehousesForItem) {
        PackageUnit[] pus = order.getPackageUnit();
        List<PackageUnit> updatedPackageUnits = new ArrayList<PackageUnit>();
        for (PackageUnit pu : pus) {
            List<OrderItem> removedItems = warehousesForItem.get(pu.getWarehouse());
            if (removedItems == null)
                continue; // this warehouse package was not updated
            OrderItem[] previousItems = pu.getOrderItem();
            List<OrderItem> updatedOrderItems = new ArrayList<OrderItem>();
            for (OrderItem previousItem : previousItems) {
                OrderItem foundItem = null; // after search if null -> item not changed by this update
                for (OrderItem removedItem : removedItems) {
                    if (previousItem.getName().equals(removedItem.getName())) {
                        foundItem = removedItem;
                        break;
                    }
                }
                int remainingQuantity = 0;
                if (foundItem == null) {
                    remainingQuantity = previousItem.getQty();
                    foundItem = previousItem;
                } else {
                    remainingQuantity = previousItem.getQty() - foundItem.getQty();
                }
                if (remainingQuantity > 0) {
                    foundItem.setQty(remainingQuantity);
                    updatedOrderItems.add(foundItem);
                }
                // else implicitly remove it by not adding to the new list
            }
            if (updatedOrderItems.size() == 0) {
                // must remove package unit from order cause no items left there
                pu.setOrderItem(null);
            } else {
                pu.setOrderItem(updatedOrderItems.toArray(new OrderItem[0]));
            }
        }
        List<PackageUnit> newPackageUnits = new ArrayList<PackageUnit>();
        for (PackageUnit pu : pus) {
            if (pu.getOrderItem() != null) {
                newPackageUnits.add(pu);
            } else { // update the list of unused warehouses for this order !
                List<String> unusedWarehouses = unusedWarehouseCache.get(order.getId());
                if (unusedWarehouses == null) {
                    unusedWarehouses = new ArrayList<String>();
                }
                unusedWarehouses.add(0, pu.getWarehouse().getCity());
                // with time the ordering is messed up because the removed warehouses
                // are added not necessarily in the correct order but in any case will be closer to client than the ones
                // inserted initially
                unusedWarehouseCache.put(order.getId(), unusedWarehouses);
            }
        }
        order.setPackageUnit(newPackageUnits.toArray(new PackageUnit[0]));
    }

    private UpdateStatusResponse addItems(Order order, OrderItem[] items) throws InternalFaultMessage {
        // try first to add the items using warehouses in the existing list in the order,
        // if not, use the cached warehouses
        UpdateStatusResponse response = new UpdateStatusResponse();
        List<Location> warehouses = new ArrayList<Location>();
        for (PackageUnit pu : order.getPackageUnit()) {
            warehouses.add(pu.getWarehouse());
        }
        List<String> unusedWarehouseCities = unusedWarehouseCache.get(order.getId());
        if (unusedWarehouseCities != null) { // order was made but all warehouses were used
            for (String unusedWarehouseCity : unusedWarehouseCities) {
                Location unusedLocation = new Location();
                unusedLocation.setCity(unusedWarehouseCity);
                warehouses.add(unusedLocation);
            }
        }
        Order addedItemsOrder = null;
        try {
            addedItemsOrder = bookOrder(order.getClientid(), order.getDestination(), items, warehouses.toArray(new Location[0]));
        } catch (UnsuccessfulOrderStatusMessage unsuccessfulOrderStatusMessage) {
            response.setStatus(UpdateStatus.CANCELLED);
            UpdateIssues issues = new UpdateIssues();
            OrderItem[] missingItems = unsuccessfulOrderStatusMessage.getFaultMessage().getOrderStatus().getOrderStatusChoice_type0().getMissingItem();
            if (missingItems == null) {
                // probably the client supplied bad item names
                String[] badItemReasons = unsuccessfulOrderStatusMessage.getFaultMessage().getOrderStatus().getOrderStatusChoice_type0().getBadItemReason();
                for (String badItemReason : badItemReasons) {
                    issues.addIssue(badItemReason);
                }
            } else {
                for (OrderItem missingItem : missingItems) {
                    issues.addIssue("For Item: '" + missingItem.getName() + "' the following quantity is not available = " + missingItem.getQty());
                }
            }
            response.setIssues(issues);
            return response;
        }
        // no exceptions -> apply the changes to the original order and discard the new order.
        unusedWarehouseCache.remove(addedItemsOrder.getId());// remove the cached warehouses for this fake order
        updateWarehousesInOrderAfterAddition(order, addedItemsOrder.getPackageUnit());
        response.setStatus(UpdateStatus.OK);
        return response;
    }

    private void updateWarehousesInOrderAfterAddition(Order order, PackageUnit[] addedOrderPackageUnits) {
        List<PackageUnit> updatedPackageUnits = new ArrayList<PackageUnit>();
        {
            PackageUnit[] previousPackageUnits = order.getPackageUnit();
            // add pus that where already in the previous order
            for (PackageUnit previousPackageUnit : previousPackageUnits) {
                updatedPackageUnits.add(previousPackageUnit);
            }
        }
        // apply the updates from added items:
        /*
        * For each added PackageUnit
        * 1. If new warehouse detected -> add the package unit directly because it is new
        * 2. If same warehouse:
        *   2.1 If new items added -> add the item to the existing package unit
        *   2.2 If item found -> add the quantities of previous item and added item
        * */

        for (PackageUnit addedOrderPU : addedOrderPackageUnits) {
            boolean packageUnitFound = false;
            for (PackageUnit existingPU : updatedPackageUnits) {
                if (addedOrderPU.getWarehouse().getCity().equals(existingPU.getWarehouse().getCity())) {
                    packageUnitFound = true;
                    for (OrderItem addItem : addedOrderPU.getOrderItem()) {
                        boolean updatedItemFound = false;
                        for (OrderItem existingItemFromPU : existingPU.getOrderItem()) {
                            if (addItem.getName().equals(existingItemFromPU.getName())) {
                                updatedItemFound = true;
                                existingItemFromPU.setQty(addItem.getQty() + existingItemFromPU.getQty());
                                break;
                            }
                        }
                        if (updatedItemFound == false) // new one was added
                        {
                            // extract the old array, add it into new list, add new item to list, set updated list on existingPU ...
                            List<OrderItem> updatedItems = new ArrayList<OrderItem>(Arrays.asList(existingPU.getOrderItem()));
                            updatedItems.add(addItem);
                            existingPU.setOrderItem(updatedItems.toArray(new OrderItem[0]));
                        }
                    }
                    break;
                }
            }
            if (packageUnitFound == false) {
                updatedPackageUnits.add(addedOrderPU);
                List<String> unusedWarehouses = unusedWarehouseCache.get(order.getId());// cannot be null -> otherwise new warehouses couldn't be selected !
                String newWarehouse = addedOrderPU.getWarehouse().getCity().intern();
                for (int i = 0; i < unusedWarehouses.size(); i++) {
                    if (newWarehouse.equals(unusedWarehouses.get(i))) {
                        unusedWarehouses.remove(i);
                        unusedWarehouseCache.put(order.getId(), unusedWarehouses);
                        break;
                    }
                }
            }
        }
        order.setPackageUnit(updatedPackageUnits.toArray(new PackageUnit[0]));
    }

    private void undoChanges(List<PackageUnit> packageUnits) {
        for (PackageUnit pu : packageUnits) {
            try {
                queryService.removeBookedItems(Arrays.asList(pu.getOrderItem()), pu.getWarehouse().getCity());
            } catch (Exception e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }
    }
}
